#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <event2/event.h>
#include <event2/http.h>
#include <event2/buffer.h>
#include <event2/util.h>
#include <event2/keyvalq_struct.h>
#include <errno.h>

#include "main.h"
#include "xmalloc.h"
#include "dict.h"
#include "httpd.h"
#include "cookie.h"
#include "log.h"
#include "tag.h"
#include "var.h"
#include "request.h"
#include "template.h"

#ifdef ENABLE_PTHREADS
# include <pthread.h>
# include <event2/thread.h>
# include "thread.h"
#endif

typedef struct httpd_thrd {
  struct event_base *base;
  struct evhttp *http;
} httpd_thrd_t;

static int htfd;
#ifndef ENABLE_PTHREADS
static httpd_thrd_t *evhttpd;
#endif


void
httpd_default_header(struct evhttp_request *req, dict_t *hdr, ck_map_t cookies)
{
  dict_elem_t *elem;
  struct evkeyvalq *headers = evhttp_request_get_output_headers(req);
  cookie_obj_t *cks;
  char *key, *tmp;
  int idx = 0;

  if (cookies != NULL) {
    while (cookie_iterate(cookies, &idx, &key, &cks) == true) {
      if (cks) {
        asprintf(&tmp, "%s=%s", key, cks->val);
        evhttp_add_header(headers, "Set-Cookie", tmp);
        RD("[COOKIE] %s: %s", key, cks->val);
        free(tmp);
      }
    }
  }
  for (size_t idx = 0; idx < dict_len(hdr); idx++) {
    elem = get_dict_elem(hdr, idx);
    RD("[HTTPD_HDR] %s: %s", elem->key, elem->value);
    evhttp_add_header(headers, elem->key, elem->value);
  }

  evhttp_add_header(headers, "Server", PACKAGE_NAME " (" PACKAGE_VERSION ")");
}

void
httpd_out(struct evhttp_request *req, string_t *outbuf, int code, const char *reason)
{
  char *sz = NULL;
  struct evbuffer *buf = evbuffer_new();

  evbuffer_add(buf, get_string(outbuf), get_strlen(outbuf));
  if ( asprintf(&sz, "%lu", (long unsigned int) evbuffer_get_length(buf)) < 0 )
    sz = "";
  evhttp_add_header(evhttp_request_get_output_headers(req), "Content-Length", sz);
  evhttp_send_reply(req, code, reason, buf);
  if (sz) free(sz);
  evbuffer_free(buf);
}

static void
send_document_cb(struct evhttp_request *req, void *arg __attribute__((__unused__)))
{
  char *qry = NULL, *peer = NULL;
  uint16_t peer_port = 0;
  const char *uri = evhttp_request_get_uri(req);
  const char *cmdtype, *path;
  struct evhttp_uri *decoded = NULL;
  struct evhttp_connection *con = evhttp_request_get_connection(req);
  request_t *r = NULL;
  struct evkeyvalq *headers;
  struct evkeyval *header;
  dict_t *http_hdr = create_dict(true);

  switch (evhttp_request_get_command(req)) {
    case EVHTTP_REQ_GET: cmdtype = "GET"; break;
    case EVHTTP_REQ_POST: cmdtype = "POST"; break;
    case EVHTTP_REQ_HEAD:
    case EVHTTP_REQ_PUT:
    case EVHTTP_REQ_DELETE:
    case EVHTTP_REQ_OPTIONS:
    case EVHTTP_REQ_TRACE:
    case EVHTTP_REQ_CONNECT:
    case EVHTTP_REQ_PATCH:
    default: cmdtype = NULL; break;
  }

  /* get http header */
  headers = evhttp_request_get_input_headers(req);
  for (header = headers->tqh_first; header; header = header->next.tqe_next) {
    insert_s(http_hdr, header->key, header->value);
  }

  /* Decode the URI */
  decoded = evhttp_uri_parse(uri);
  if (!decoded) {
#ifdef ENABLE_PTHREADS
    W("[%d] [%lu] %s: It's not a good URI. Sending BADREQUEST", get_thread_num(), requests_processed(), uri);
#else
    W("[%lu] %s: It's not a good URI. Sending BADREQUEST", requests_processed(), uri);
#endif
    evhttp_send_error(req, HTTP_BADREQUEST, 0);
    return;
  }

  path = evhttp_uri_get_path(decoded);
  if (!path) path = "/";

  qry = (char *)(uri + strlen(path));
  if (*qry != '\0') qry++;
#ifdef ENABLE_PTHREADS
  r = request_init(get_thread_num(), qry, cmdtype, path, (void *)req, true, http_hdr);
#else
  r = request_init(0, qry, cmdtype, path, (void *)req, true, http_hdr);
#endif
  evhttp_connection_get_peer(con, &peer, &peer_port);
  RD("[%lu] client %s:%d connected", (long unsigned int)r->req_num, peer, peer_port);
#if defined(ENABLE_DEBUG) && defined(ENABLE_REQDBG)
  print_request_debug(r);
#endif
  process_request(r);
  xmemstat_print(r->thrd_num, r->req_num);
  request_free(r);
  if (decoded)
    evhttp_uri_free(decoded);
}

int
check_version_match(void)
{
  ev_uint32_t v_compile, v_run;
  v_compile = LIBEVENT_VERSION_NUMBER;
  v_run = event_get_version_number();
  if ((v_compile & 0xffff0000) != (v_run & 0xffff0000)) {
    W("Running with a Libevent version (%s) very different from the "
      "one we were built with (%s).\n", event_get_version(), LIBEVENT_VERSION);
    return(-1);
  }
  I("Libevent version %s", event_get_version());
  return(0);
}

static void
httpd_log(int severity, const char *msg)
{
  D("evhttp(%d): %s", severity, msg);
}

static int
httpserver_bindsocket(const char *listen_addr, const uint16_t port, int backlog) {
   int r;
   int nfd;
   nfd = socket(AF_INET, SOCK_STREAM, 0);
   if (nfd < 0) return -1;

   int one = 1;
   r = setsockopt(nfd, SOL_SOCKET, SO_REUSEADDR, (char *)&one, sizeof(int));
 
   struct sockaddr_in addr;
   memset(&addr, 0, sizeof(addr));
   addr.sin_family = AF_INET;
   addr.sin_addr.s_addr = inet_addr(listen_addr);
   addr.sin_port = htons(port);

   r = bind(nfd, (struct sockaddr*)&addr, sizeof(addr));
   if (r < 0) return -1;
   r = listen(nfd, backlog);
   if (r < 0) return -1;

   int flags;
   if ((flags = fcntl(nfd, F_GETFL, 0)) < 0 || fcntl(nfd, F_SETFL, flags | O_NONBLOCK) < 0) {
    return -1;
  }
 
  return(nfd);
}

static void *
httpd_dispatch_thrd(void *arg)
{
  httpd_thrd_t *thrd = (httpd_thrd_t *) arg;

#ifdef ENABLE_PTHREADS
  I("Thread(%d): entering http event loop", get_thread_num());
#else
  I2("entering http event loop");
#endif
  init_templates_thrd();
  call_vinit_cbs();
  event_base_dispatch(thrd->base);
  call_vfree_cbs();
  templates_cleanup_thrd();
  evhttp_free(thrd->http);
  event_base_free(thrd->base);
  return(NULL);
}

bool
__httpd_run(const char *listen_addr, const uint16_t port)
{
  int idx;
#ifdef ENABLE_PTHREADS
  int nthreads = get_pthread_count();
  threadpool_t *tp;
#else
  int nthreads = 1;
#endif
  htfd = httpserver_bindsocket(listen_addr, port, 2*nthreads);
  httpd_thrd_t *hthrd;

  if (htfd < 0) {
    E_errstr("Couldn't bind to port %lu", port);
    return(false);
  }
  for (idx = 0; idx < nthreads; idx++) {
    hthrd = (httpd_thrd_t *) ncmalloc(1, sizeof(struct httpd_thrd));
#ifdef ENABLE_PTHREADS
    tp = (threadpool_t *) get_thread_data(idx);
    tp->data = (void *) hthrd;
#endif
    hthrd->base = event_base_new();
    if (!hthrd->base) {
      E2("Couldn't create an event_base: exiting");
      return (false);
    }
    /* Create a new evhttp object to handle requests. */
    hthrd->http = evhttp_new(hthrd->base);
    if (!hthrd->http) {
      E2("Couldn't create evhttp: exiting");
      return (false); 
    }
    if (evhttp_accept_socket(hthrd->http, htfd)) {
      E2("Accept socket failed: exiting");
      return (false);
    }
    /* We want to accept arbitrary requests, so we need to set a "generic"
     * cb. We can also add callbacks for specific paths. */
    evhttp_set_gencb(hthrd->http, send_document_cb, &hthrd);
    evhttp_set_timeout(hthrd->http, HTTPD_TIMEOUT);
#ifdef ENABLE_PTHREADS
    if (create_thread(httpd_dispatch_thrd, hthrd) != 0) {
      W("Thread(%d) failed to start", get_thread_num());
      return (false);
    }
#else
    evhttpd = hthrd;
    httpd_dispatch_thrd(hthrd);
#endif
  }

#ifdef ENABLE_PTHREADS
  threads_wait(sizeof(struct httpd_thrd));
#endif
  return (true);
}

bool
httpd_run(const char *listen_addr, const uint16_t port)
{
  if (check_version_match() != 0) {
    E2("libevent version mismatch");
    return (false);
  }
#ifdef ENABLE_DEBUG
  event_enable_debug_mode();
#endif
#ifdef ENABLE_PTHREADS
  if (evthread_use_pthreads() != 0) {
    E2("libevent does NOT support pthreads, abort ..");
    return (false);
  }
 #ifdef ENABLE_DEBUG
  evthread_enable_lock_debuging();
 #endif
#endif
  event_set_log_callback(httpd_log);
  return (__httpd_run(listen_addr, port));
}

void
httpd_exit(void) {
#ifdef ENABLE_PTHREADS
  int idx;
  I2("try to cancel all httpd threads");
  threads_mtx_lock();
  for (idx = 0; idx < get_pthread_count(); idx++)
    event_base_loopexit(((httpd_thrd_t *)get_thread_data(idx)->data)->base, NULL);
  threads_mtx_unlock();
#else
  I2("try to cancel evhttpd");
  event_base_loopexit(evhttpd->base, NULL);
#endif
}
